Kealdish's Studio.

Weak 属性在 dealloc() 背后的逻辑

字数统计: 962阅读时长: 4 min
2017/10/04 Share

前言

我们都知道在 ARC 环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理。然而,最近在项目中遇到这样一个问题,代码示例如下:

1
2
3
4
5
6
// class Foo
- (void)dealloc {
if (_obj) {
[_obj removeObserver:[KVOObserverStub stub] forKeyPath:_keyPath];
}
}

其中,_obj 是类 Foo 的 weak 属性,这段代码所做的工作是在 Foo 的实例对象销毁的时候,移除 _obj 对象上的观察者,否则,会导致 KVO still registering when deallocated 的 crash 。原以为这样写会 work ,然而在实际测试中发现,在触发 dealloc 方法时,_obj 此时已被置为 nil ,即 [_obj removeObserver:[KVOObserverStub stub] forKeyPath:_keyPath]; 并不会触发。解决方案则是将 _obj 属性的修饰符由 weak 改为 unsafe_unretained 或者 assign 。但是,这样改可能会为其他代码引入 bug ,因为现在 ARC 不会自动为将 _obj 对象置为 nil ,需要去检查涉及 _obj 部分的代码。虽然这样解决问题了,但是,我还是对 weak 属性在其持有对象的 dealloc 方法触发时背后的逻辑很感兴趣。

探究

llvm 官方的 ARC 文档中对 ARC 下的 dealloc 过程做了简单说明:

A class may provide a method definition for an instance method named dealloc. This method will be called after the final release of the object but before it is deallocated or any of its instance variables are destroyed. The superclass’s implementation of dealloc will be called automatically when the method returns.

大概意思是: dealloc 方法在最后一次 release 后被调用,但此时实例变量(Ivars)并未释放,父类的 dealloc 的方法将在子类 dealloc 方法返回后自动调用

The instance variables for an ARC-compiled class will be destroyed at some point after control enters the dealloc method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.

理解: ARC 下对象的实例变量在根类 [NSObject dealloc] 中释放。

为了探究背后的逻辑,我写了一个例子,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface A
@property (nonatomic, weak) id refObject;
@end
@implementation A
- (void)foo {
B* b = [B new];
self.refObject = b;
// Just use b after the weak assignment
// in order to not dealloc 'b' before assignement
NSLog(@"%@", b);
}
@end
1
2
3
4
5
6
7
8
@interface B
@end
@implementation B
- (void)dealloc {
NSLog(@"In dealloc");
}
@end

上面的代码意思大致是类 A 有一个 weak 属性 refObject ,在 foo() 方法中将 refObject 设置为 b 。运行代码,并在 dealloc 方法调用时断点观察,我们会发现在 dealloc 方法执行的最后,a->_refObject 被置为 nil ,调用栈大致是这样的:

1
2
3
4
5
6
7
8
frame #0: 0x00007fff8ab9f0f8 libobjc.A.dylib`arr_clear_deallocating + 83
frame #1: 0x00007fff8ab889ee libobjc.A.dylib`objc_clear_deallocating + 151
frame #2: 0x00007fff8ab88940 libobjc.A.dylib`objc_destructInstance + 121
frame #3: 0x00007fff8ab88fa0 libobjc.A.dylib`object_dispose + 22
frame #4: 0x0000000100000b27 weakdealloc`-[B dealloc](self=0x000000010010a640, _cmd=0x00007fff887f807b) + 151 at main.m:28
frame #5: 0x0000000100000bbc weakdealloc`-[A foo](self=0x0000000100108290, _cmd=0x0000000100000e6f) + 140 at main.m:41
frame #6: 0x0000000100000cf5 weakdealloc`main(argc=1, argv=0x00007fff5fbff968) + 117 at main.m:52
frame #7: 0x00007fff8c0987e1 libdyld.dylib`start + 1

调用栈中的 object_dispose 方法是在 -[NSObject dealloc] 方法中调用的,我们可以从 NSObject.mm 看出。因而,在调用 -[B dealloc] 方法时,在 -[NSObject dealloc] 方法调用之前,a->_refObject 都不为 nil 。那么问题来了,为什么我们在 -[B dealloc] 方法中访问 a.refObject 是为 nil 呢?

查阅 LLVM文档 时发现,原来是 objc_loadWeakRetained 方法在作祟:

id objc_loadWeakRetained(id *object)


If object is registered as a __weak object, and the last value stored into object has not yet been deallocated or begun deallocation, retains that value and returns it. Otherwise, returns null.

这段文档意思是 weak 对象在开始 deallocate 后,objc_loadWeakRetained 会返回 nil 。因而,我们无法通过 a._refObject 去访问对象。那么为什么可以通过 a->refObject 去访问到呢?其实,很简单,因为 debugger 通过 a->refObject 访问对象时,并没有调用 objc_loadWeak() 方法。

结论

dealloc 操作开始后,在 -[B dealloc] 中实例对象 a->refObejct 并不为 nil ,但是指向该对象的 weak 指针经过 ARC runtime 函数的处理已经返回 nil

CATALOG
  1. 1. 前言
  2. 2. 探究
  3. 3. 结论